Italiano

Esplora i tipi branded di TypeScript, una potente tecnica per ottenere la tipizzazione nominale in un sistema di tipi strutturale. Impara come migliorare la sicurezza dei tipi e la chiarezza del codice.

Tipi Branded in TypeScript: Tipizzazione Nominale in un Sistema Strutturale

Il sistema di tipi strutturale di TypeScript offre flessibilità, ma a volte può portare a comportamenti inaspettati. I tipi branded (o "marcati") forniscono un modo per imporre la tipizzazione nominale, migliorando la sicurezza dei tipi e la chiarezza del codice. Questo articolo esplora i tipi branded in dettaglio, fornendo esempi pratici e best practice per la loro implementazione.

Comprendere la Tipizzazione Strutturale e Nominale

Prima di immergerci nei tipi branded, chiariamo la differenza tra tipizzazione strutturale e nominale.

Tipizzazione Strutturale (Duck Typing)

In un sistema di tipi strutturale, due tipi sono considerati compatibili se hanno la stessa struttura (cioè, le stesse proprietà con gli stessi tipi). TypeScript utilizza la tipizzazione strutturale. Considera questo esempio:


interface Point {
  x: number;
  y: number;
}

interface Vector {
  x: number;
  y: number;
}

const point: Point = { x: 10, y: 20 };
const vector: Vector = point; // Valido in TypeScript

console.log(vector.x); // Output: 10

Anche se Point e Vector sono dichiarati come tipi distinti, TypeScript consente di assegnare un oggetto Point a una variabile Vector perché condividono la stessa struttura. Questo può essere comodo, ma può anche portare a errori se è necessario distinguere tra tipi logicamente diversi che hanno casualmente la stessa forma. Ad esempio, pensando a coordinate di latitudine/longitudine che potrebbero incidentalmente corrispondere a coordinate di pixel dello schermo.

Tipizzazione Nominale

In un sistema di tipi nominale, i tipi sono considerati compatibili solo se hanno lo stesso nome. Anche se due tipi hanno la stessa struttura, sono trattati come distinti se hanno nomi diversi. Linguaggi come Java e C# utilizzano la tipizzazione nominale.

La Necessità dei Tipi Branded

La tipizzazione strutturale di TypeScript può essere problematica quando è necessario garantire che un valore appartenga a un tipo specifico, indipendentemente dalla sua struttura. Ad esempio, considera la rappresentazione delle valute. Potresti avere tipi diversi per USD ed EUR, ma entrambi potrebbero essere rappresentati come numeri. Senza un meccanismo per distinguerli, potresti accidentalmente eseguire operazioni sulla valuta sbagliata.

I tipi branded risolvono questo problema consentendo di creare tipi distinti che sono strutturalmente simili ma trattati come diversi dal sistema di tipi. Ciò migliora la sicurezza dei tipi e previene errori che altrimenti potrebbero passare inosservati.

Implementare i Tipi Branded in TypeScript

I tipi branded vengono implementati utilizzando i tipi di intersezione e un simbolo univoco o un letterale di stringa. L'idea è di aggiungere un "marchio" (brand) a un tipo che lo distingua da altri tipi con la stessa struttura.

Utilizzo dei Simboli (Consigliato)

L'uso di simboli per il branding è generalmente preferito perché i simboli sono garantiti come univoci.


const USD = Symbol('USD');
type USD = number & { readonly [USD]: unique symbol };

const EUR = Symbol('EUR');
type EUR = number & { readonly [EUR]: unique symbol };

function createUSD(value: number): USD {
  return value as USD;
}

function createEUR(value: number): EUR {
  return value as EUR;
}

function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD;
}

const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);

const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);

// Decommentare la riga successiva causerà un errore di tipo
// const invalidOperation = addUSD(usd1, eur1);

In questo esempio, USD e EUR sono tipi branded basati sul tipo number. Il unique symbol garantisce che questi tipi siano distinti. Le funzioni createUSD e createEUR vengono utilizzate per creare valori di questi tipi, e la funzione addUSD accetta solo valori USD. Tentare di aggiungere un valore EUR a un valore USD risulterà in un errore di tipo.

Utilizzo dei Letterali di Stringa

È anche possibile utilizzare i letterali di stringa per il branding, sebbene questo approccio sia meno robusto rispetto all'uso dei simboli, poiché non è garantito che i letterali di stringa siano univoci.


type USD = number & { readonly __brand: 'USD' };
type EUR = number & { readonly __brand: 'EUR' };

function createUSD(value: number): USD {
  return value as USD;
}

function createEUR(value: number): EUR {
  return value as EUR;
}

function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD;
}

const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);

const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);

// Decommentare la riga successiva causerà un errore di tipo
// const invalidOperation = addUSD(usd1, eur1);

Questo esempio ottiene lo stesso risultato del precedente, ma utilizzando letterali di stringa invece di simboli. Sebbene più semplice, è importante assicurarsi che i letterali di stringa utilizzati per il branding siano univoci all'interno della propria codebase.

Esempi Pratici e Casi d'Uso

I tipi branded possono essere applicati a vari scenari in cui è necessario imporre la sicurezza dei tipi oltre la compatibilità strutturale.

ID

Considera un sistema con diversi tipi di ID, come UserID, ProductID e OrderID. Tutti questi ID potrebbero essere rappresentati come numeri o stringhe, ma si vuole impedire la mescolanza accidentale di tipi di ID diversi.


const UserIDBrand = Symbol('UserID');
type UserID = string & { readonly [UserIDBrand]: unique symbol };

const ProductIDBrand = Symbol('ProductID');
type ProductID = string & { readonly [ProductIDBrand]: unique symbol };

function getUser(id: UserID): { name: string } {
  // ... recupera i dati dell'utente
  return { name: "Alice" };
}

function getProduct(id: ProductID): { name: string, price: number } {
  // ... recupera i dati del prodotto
  return { name: "Example Product", price: 25 };
}

function createUserID(id: string): UserID {
  return id as UserID;
}

function createProductID(id: string): ProductID {
  return id as ProductID;
}

const userID = createUserID('user123');
const productID = createProductID('product456');

const user = getUser(userID);
const product = getProduct(productID);

console.log("User:", user);
console.log("Product:", product);

// Decommentare la riga successiva causerà un errore di tipo
// const invalidCall = getUser(productID);

Questo esempio dimostra come i tipi branded possano impedire di passare un ProductID a una funzione che si aspetta un UserID, migliorando la sicurezza dei tipi.

Valori Specifici del Dominio

I tipi branded possono anche essere utili per rappresentare valori specifici del dominio con dei vincoli. Ad esempio, potresti avere un tipo per le percentuali che dovrebbe essere sempre compreso tra 0 e 100.


const PercentageBrand = Symbol('Percentage');
type Percentage = number & { readonly [PercentageBrand]: unique symbol };

function createPercentage(value: number): Percentage {
  if (value < 0 || value > 100) {
    throw new Error('Percentage must be between 0 and 100');
  }
  return value as Percentage;
}

function applyDiscount(price: number, discount: Percentage): number {
  return price * (1 - discount / 100);
}

try {
  const discount = createPercentage(20);
  const discountedPrice = applyDiscount(100, discount);
  console.log("Discounted Price:", discountedPrice);

  // Decommentare la riga successiva causerà un errore durante l'esecuzione
  // const invalidPercentage = createPercentage(120);
} catch (error) {
  console.error(error);
}

Questo esempio mostra come imporre un vincolo sul valore di un tipo branded durante l'esecuzione. Sebbene il sistema di tipi non possa garantire che un valore Percentage sia sempre compreso tra 0 e 100, la funzione createPercentage può imporre questo vincolo in fase di esecuzione. È anche possibile utilizzare librerie come io-ts per imporre la validazione a runtime dei tipi branded.

Rappresentazioni di Data e Ora

Lavorare con date e orari può essere complicato a causa dei vari formati e fusi orari. I tipi branded possono aiutare a differenziare tra diverse rappresentazioni di data e ora.


const UTCDateBrand = Symbol('UTCDate');
type UTCDate = string & { readonly [UTCDateBrand]: unique symbol };

const LocalDateBrand = Symbol('LocalDate');
type LocalDate = string & { readonly [LocalDateBrand]: unique symbol };

function createUTCDate(dateString: string): UTCDate {
  // Valida che la stringa della data sia in formato UTC (es. ISO 8601 con Z)
  if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(dateString)) {
    throw new Error('Invalid UTC date format');
  }
  return dateString as UTCDate;
}

function createLocalDate(dateString: string): LocalDate {
  // Valida che la stringa della data sia in formato data locale (es. YYYY-MM-DD)
  if (!/\d{4}-\d{2}-\d{2}/.test(dateString)) {
    throw new Error('Invalid local date format');
  }
  return dateString as LocalDate;
}

function convertUTCDateToLocalDate(utcDate: UTCDate): LocalDate {
  // Esegui la conversione del fuso orario
  const date = new Date(utcDate);
  const localDateString = date.toLocaleDateString();
  return createLocalDate(localDateString);
}

try {
  const utcDate = createUTCDate('2024-01-20T10:00:00.000Z');
  const localDate = convertUTCDateToLocalDate(utcDate);
  console.log("UTC Date:", utcDate);
  console.log("Local Date:", localDate);
} catch (error) {
  console.error(error);
}

Questo esempio differenzia tra date UTC e locali, assicurando che si stia lavorando con la rappresentazione corretta di data e ora in diverse parti dell'applicazione. La validazione a runtime garantisce che solo le stringhe di data formattate correttamente possano essere assegnate a questi tipi.

Best Practice per l'Uso dei Tipi Branded

Per utilizzare efficacemente i tipi branded in TypeScript, considera le seguenti best practice:

Vantaggi dei Tipi Branded

Svantaggi dei Tipi Branded

Alternative ai Tipi Branded

Sebbene i tipi branded siano una tecnica potente per ottenere la tipizzazione nominale in TypeScript, esistono approcci alternativi che potresti considerare.

Tipi Opachi (Opaque Types)

I tipi opachi sono simili ai tipi branded ma forniscono un modo più esplicito per nascondere il tipo sottostante. TypeScript non ha un supporto integrato per i tipi opachi, ma è possibile simularli utilizzando moduli e simboli privati.

Classi

L'uso delle classi può fornire un approccio più orientato agli oggetti per definire tipi distinti. Sebbene le classi siano tipizzate strutturalmente in TypeScript, offrono una separazione delle responsabilità più chiara e possono essere utilizzate per imporre vincoli tramite i metodi.

Librerie come `io-ts` o `zod`

Queste librerie forniscono una validazione dei tipi a runtime sofisticata e possono essere combinate con i tipi branded per garantire la sicurezza sia in fase di compilazione che di esecuzione.

Conclusione

I tipi branded di TypeScript sono uno strumento prezioso per migliorare la sicurezza dei tipi e la chiarezza del codice in un sistema di tipi strutturale. Aggiungendo un "marchio" (brand) a un tipo, è possibile imporre la tipizzazione nominale e prevenire la mescolanza accidentale di tipi strutturalmente simili ma logicamente diversi. Sebbene i tipi branded introducano una certa complessità e overhead, i benefici di una maggiore sicurezza dei tipi e manutenibilità del codice spesso superano gli svantaggi. Considera l'utilizzo dei tipi branded in scenari in cui è necessario garantire che un valore appartenga a un tipo specifico, indipendentemente dalla sua struttura.

Comprendendo i principi alla base della tipizzazione strutturale e nominale e applicando le best practice descritte in questo articolo, puoi sfruttare efficacemente i tipi branded per scrivere codice TypeScript più robusto e manutenibile. Dalla rappresentazione di valute e ID all'imposizione di vincoli specifici del dominio, i tipi branded forniscono un meccanismo flessibile e potente per migliorare la sicurezza dei tipi nei tuoi progetti.

Mentre lavori con TypeScript, esplora le varie tecniche e librerie disponibili per la validazione e l'imposizione dei tipi. Considera l'utilizzo dei tipi branded in combinazione con librerie di validazione a runtime come io-ts o zod per ottenere un approccio completo alla sicurezza dei tipi.